package com.google.code.lf.gfm.service.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import com.google.code.lf.commons.aspect.Loggable;
import com.google.code.lf.commons.exception.InvalidArgumentException;
import com.google.code.lf.commons.exception.NullArgumentException;
import com.google.code.lf.commons.util.FileHelper;
import com.google.code.lf.commons.xml.bind.handler.CustomCharacterEscapeHandler;
import com.google.code.lf.commons.xml.bind.helper.XmlFileHelper;
import com.google.code.lf.gfm.model.grisbi.GrisbiFile;
import com.google.code.lf.gfm.service.IGrisbiFileService;
import com.google.code.lf.gfm.service.IGrisbiFileServiceLocal;

/**
 * Implémentation du service de manipulation de fichiers de données <a href="http://grisbi.org">Grisbi</a>.
 * <br/>Le binding bean/xml utilise <a href="https://jaxb.dev.java.net/">JAXB</a>.
 *
 * @author gael.lorent-fonfrede
 */
@Loggable
@Service(IGrisbiFileService.BEAN_ID)
public class GrisbiFileService implements IGrisbiFileService, IGrisbiFileServiceLocal {
	/** Logger. */
	private static final Logger logger = Logger.getLogger(GrisbiFileService.class);

	/** {@link JAXBContext}, la classe est thread safe et peut-être partagée. */
	private static JAXBContext jaxbContext = XmlFileHelper.initializeJaxbContext(GrisbiFile.class);

	/** Instance de gestionnaire de caractère d'échappements à utiliser pour les {@link GrisbiFile}. */
	private static CustomCharacterEscapeHandler customCharacterEscapeHandler = new CustomCharacterEscapeHandler();

	/* (non-Javadoc)
	 * @see com.google.code.lf.gfm.service.IGrisbiFileService#readGrisbiFile(java.io.File)
	 */
	public GrisbiFile readGrisbiFile(final File xmlFile) {
		// Lecture, parsing du fichier fourni et récupération du bean model correspondant
		final GrisbiFile gsbFile = XmlFileHelper.readFile(jaxbContext, xmlFile, GrisbiFile.class);

		// Vérification de la version lue (la modélisation est fortement adhérente à la version applicative)
		final String grisbiVersion = gsbFile.getGeneralites().getVersionGrisbi();
		if (GrisbiFile.GrisbiSupportedVersion.compareToIgnoreCase(grisbiVersion) > 0)
			logger.warn(new StringBuilder("Grisbi version check failed ! Expected: ").append(GrisbiFile.GrisbiSupportedVersion).append(", read: ").append(grisbiVersion).append("."));

		// Conservation de la référence du fichier source
		gsbFile.setFileReference(xmlFile);

		return gsbFile;
	}

	/* (non-Javadoc)
	 * @see com.google.code.lf.gfm.service.IGrisbiFileService#writeGrisbiFile(com.google.code.lf.gfm.model.grisbi.GrisbiFile, java.io.File, java.lang.String)
	 */
	public void writeGrisbiFile(final GrisbiFile gsbFile, final File xmlFile, final String encoding) {
		// Validation des paramètres
		if (gsbFile == null) throw new NullArgumentException("gsbFile");
		FileHelper.checkFileExistsAndIsReadable(xmlFile);
		XmlFileHelper.checkParamEncoding(encoding, GrisbiFile.GrisbiFileEncoding);

		// Initialisation du marshaller
		final Marshaller marshaller = XmlFileHelper.initializeMarshaller(jaxbContext, true, encoding, GrisbiFile.grisbiIndentString, customCharacterEscapeHandler);

		// Ecriture de l'objet model fourni sous forme de fichier xml
		XmlFileHelper.writeFile(marshaller, gsbFile, xmlFile);

		// Post-traitement du fichier xml (spécifique et optionnel)
		grisbiFilePostProcess(xmlFile);
	}

	/* (non-Javadoc)
	 * @see com.google.code.lf.gfm.service.IGrisbiFileServiceLocal#writeGrisbiFileOnSystemOut(com.google.code.lf.gfm.model.grisbi.GrisbiFile, java.lang.String)
	 */
	public void writeGrisbiFileOnSystemOut(final GrisbiFile gsbFile, final String encoding) {
		// Validation des paramètres
		if (gsbFile == null) throw new NullArgumentException("gsbFile");
		XmlFileHelper.checkParamEncoding(encoding, GrisbiFile.GrisbiFileEncoding);

		// Initialisation du marshaller
		final Marshaller marshaller = XmlFileHelper.initializeMarshaller(jaxbContext, true, encoding, GrisbiFile.grisbiIndentString, customCharacterEscapeHandler);

		// Ecriture de l'objet model fourni sur la console
		XmlFileHelper.writeFile(marshaller, gsbFile, System.out);
	}

	/**
	 * Post-traitement d'un fichier grisbi généré.
	 * <br/>Noter que Grisbi peut tout de même ouvrir les fichiers sans ce post-traitement mais l'objectif reste de restituer un fichier identique à l'original.
	 * <br/>En outre, cela facilite l'aspect tests unitaires.
	 * <br/>
	 * <br/>Gère notamment le remplacement des valeurs numériques des des caractères échappés en valeur hexadécimale.
	 * 
	 * @param xmlFile Le fichier à traiter.
	 */
	private void grisbiFilePostProcess(final File xmlFile) {
		// Validation des paramètres
		if ((xmlFile == null) || (xmlFile.exists() && (!xmlFile.isFile() || !xmlFile.canWrite()))) throw new InvalidArgumentException("xmlFile", "Provided parameter is null or invalid.");
		BufferedReader br = null;
		PrintWriter pw = null;

		try {
			boolean isFileModified = false;
			final List<String> modifiedFileContent = new ArrayList<String>();

			// Lecture du fichier
			br = new BufferedReader(new FileReader(xmlFile));
			String currentLine = br.readLine();

			// Traitement ligne à ligne
			while (currentLine != null) {
				// Traitement de l'en-tête xml
				if (StringUtils.indexOf(currentLine, "?xml") > 0) {
					isFileModified = true;
					currentLine = "<?xml version=\"1.0\"?>";
				}

				// Traitement des xsi:nil="true" (cas par ex. des @XmlElement(... nillable=true))
				if (StringUtils.indexOf(currentLine, "xsi:nil") > 0) {
					isFileModified = true;
					currentLine = StringUtils.replace(currentLine, " xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
				}

				// Détection et traitement récursif des caractères d'échappement
				// !! Avantageusement remplacé par le CustomCharacterEscapeHandler dans initializeMarshaller() !!
				/*if (StringUtils.indexOf(currentLine, "&#") > 0) {
					isFileModified = true;
					final StringTokenizer tokenizer = new StringTokenizer(currentLine, "&#");
					tokenizer.nextToken();
					while (tokenizer.hasMoreTokens()) {
						// Remplacement de la valeur numérique par son équivalent en hexa
						final String token = tokenizer.nextToken();
						final String escapedNumericValue = StringUtils.substringBefore(token, ";");
						if (StringUtils.isNumeric(escapedNumericValue)) {
							final String escapedHexValue = "x" + Integer.toHexString(Integer.valueOf(escapedNumericValue)).toUpperCase();
							currentLine = StringUtils.replace(currentLine, escapedNumericValue, escapedHexValue);
						}
					}
				}*/

				modifiedFileContent.add(currentLine);
				currentLine = br.readLine();
			}

			// Réécriture du fichier
			if (isFileModified) {
				pw = new PrintWriter(xmlFile);
				for (final String line : modifiedFileContent)
					pw.println(line);
			}
		} catch (final IOException e) {
			logger.fatal(new StringBuilder("IOException raised: ").append(e.getMessage()).toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		} finally {
			try {
				if (br != null)
					br.close();
			} catch (final Exception ex) {
			}
			try {
				if (pw != null)
					pw.close();
			} catch (final Exception ex) {
			}
		}
	}
}
